/*
 * Copyright 2017-2022 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.expressions.util;

import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.expressions.EvaluatedExpressionReference;
import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory;
import io.micronaut.expressions.context.ExpressionEvaluationContext;
import io.micronaut.expressions.parser.CompoundEvaluatedExpressionParser;
import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.visitor.VisitorContext;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;

/**
 * Utility class for working with annotation metadata containing
 * evaluated expressions.
 *
 * @author Sergey Gavrilov
 * @since 4.0.0
 */
@Internal
public final class EvaluatedExpressionsUtils {

    /**
     * Evaluates the expression return type.
     *
     * @param visitorContext The visitor context
     * @param methodElement  The method element
     * @param reference      The expression reference
     * @return The resolved type
     * @since 4.3.0
     */
    public static ClassElement evaluateExpressionType(VisitorContext visitorContext,
                                                      MethodElement methodElement,
                                                      EvaluatedExpressionReference reference) {
        DefaultExpressionCompilationContextFactory factory = new DefaultExpressionCompilationContextFactory(visitorContext);
        ExpressionEvaluationContext context = factory.buildContextForMethod(reference, methodElement);
        String expression = (String) reference.annotationValue();
        return new CompoundEvaluatedExpressionParser(expression)
            .parse()
            .resolveClassElement(new ExpressionVisitorContext(context, visitorContext));
    }

    /**
     * Evaluates the expression return type.
     *
     * @param visitorContext The visitor context
     * @param thisElement    The this element
     * @param reference      The expression reference
     * @return The resolved type
     * @since 4.3.0
     */
    public static ClassElement evaluateExpressionType(VisitorContext visitorContext,
                                                      ClassElement thisElement,
                                                      EvaluatedExpressionReference reference) {
        DefaultExpressionCompilationContextFactory factory = new DefaultExpressionCompilationContextFactory(visitorContext);
        ExpressionEvaluationContext context = factory.buildContext(reference, thisElement);
        String expression = (String) reference.annotationValue();
        return new CompoundEvaluatedExpressionParser(expression)
            .parse()
            .resolveClassElement(new ExpressionVisitorContext(context, visitorContext));
    }

    /**
     * Finds evaluated expression references in provided annotation metadata,
     * including nested annotation values.
     *
     * @param annotationMetadata metadata to search references in
     * @return collection of expression references
     */
    public static Collection<EvaluatedExpressionReference> findEvaluatedExpressionReferences(AnnotationMetadata annotationMetadata) {
        return Stream.concat(
                annotationMetadata.getAnnotationNames().stream(),
                annotationMetadata.getStereotypeAnnotationNames().stream())
            .map(annotationMetadata::getAnnotation)
            .flatMap(annotation -> getNestedAnnotationValues(annotation).stream())
            .flatMap(av -> av.getValues().values().stream())
            .filter(EvaluatedExpressionReference.class::isInstance)
            .map(EvaluatedExpressionReference.class::cast)
            .distinct()
            .toList();
    }

    private static Collection<AnnotationValue<?>> getNestedAnnotationValues(Object value) {
        List<AnnotationValue<?>> result = new ArrayList<>();
        if (value instanceof AnnotationValue<?> annotationValue) {
            for (Object nestedValue : annotationValue.getValues().values()) {
                result.addAll(getNestedAnnotationValues(nestedValue));
            }
            result.add(annotationValue);
        } else {
            Iterable<?> nestedValues = null;
            if (value instanceof Iterable<?> iterable) {
                nestedValues = iterable;
            } else if (value instanceof AnnotationValue<?>[] values) {
                nestedValues = Arrays.asList(values);
            }
            if (nestedValues != null) {
                for (Object nextValue : nestedValues) {
                    if (nextValue instanceof AnnotationValue) {
                        result.addAll(getNestedAnnotationValues(nextValue));
                    }
                }
            }
        }
        return result;
    }
}
